package cn.lg.soar.common.algorithm.serial;

import cn.lg.soar.common.util.IpUtil;
import cn.lg.soar.common.util.asserts.InnerAssert;
import cn.lg.soar.common.util.data.DataUtil;
import cn.lg.soar.common.util.hardware.SystemUtils;

import java.time.LocalDateTime;
import java.time.ZoneId;

/**
 * 雪花算法（分布式id生成器）
 * 格式：共63位，41位时间戳，后22位由标识码和流水号组成，根据 serialBitCount 配置分配，serialBitCount 代表流水占多少位
 * 可配置时间戳开始时间计算时间（startTime）
 * @author luguoxiang
 * 开源项目：https://gitee.com/lgx1992/lg-soar 求star！请给我star！请帮我点个star！
 */
public class Snowflake {

    private final static Snowflake SNOWFLAKE = new Snowflake();

    /**
     * 前置缓存行对齐（）
     */
    private long p0,p1,p2;

    /**
     * 配置数据（初始化后不在变化）
     */
    private final int max;
    private final int characteristic;
    private final long startTime;

    /**
     * 运行数据（运行中不断改变）
     */
    private long timestamp = 0;
    private long serial = 0;
    private long serialMax = 0;

    /**
     * 后置缓存行对齐
     */
    private long p3,p4,p5;

    /**
     * 默认
     */
    public Snowflake() {
        this(12, getHostCharacteristic(), 0);
    }

    /**
     * 定制化
     * @param serialBitCount 流水号位数(bit位)（默认12位）
     * @param characteristic 主机标识（也可以是业务标识）（位数等于 22 - serialBitCount）
     * @param startTime 时间戳开始时间（默认按系统时间）
     */
    public Snowflake(int serialBitCount, int characteristic, long startTime) {
        InnerAssert.isTrue(serialBitCount >= 0 && serialBitCount <= 22, "serialBitCount must range [0, 22]");
        this.max = 1 << serialBitCount;
        this.characteristic = (characteristic << serialBitCount) & 0x3FFFFF;
        this.startTime = startTime;
    }

    /**
     * 获取下一个id
     * @return
     */
    public synchronized long next(){
        long ts = System.currentTimeMillis();
        if (ts != this.timestamp) {
            // 生成下一毫秒的数据
            long prefix = (((ts - startTime) & 0x3FF_FFFF_FFFFL) << 22) | characteristic;
            this.timestamp = ts;
            this.serial = prefix;
            this.serialMax = prefix + this.max;
            return serial++;
        }
        if (this.serial == this.serialMax) {
            throw new RuntimeException("Serial number out of range，Up to "+ this.max +" can be generated in one millisecond");
        }
        return this.serial++;
    }

    public static long nextValue(){
        return SNOWFLAKE.next();
    }

    /**
     * 建造者模式
     * @return
     */
    public static SnowflakeBuilder builder() {
        return new SnowflakeBuilder();
    }

    public static class SnowflakeBuilder {

        private Integer serialBitCount;
        private Integer characteristic;
        private long startTime;

        public SnowflakeBuilder serialBitCount(int serialBitCount){
            this.serialBitCount = serialBitCount;
            return this;
        }

        public SnowflakeBuilder characteristic(Class<?> characteristic){
            this.characteristic = characteristic.hashCode();
            return this;
        }

        public SnowflakeBuilder characteristic(String characteristic){
            this.characteristic = characteristic.hashCode();
            return this;
        }

        public SnowflakeBuilder characteristic(int characteristic){
            this.characteristic = characteristic;
            return this;
        }

        public SnowflakeBuilder startTime(long startTime){
            this.startTime = startTime;
            return this;
        }

        public SnowflakeBuilder startTime(LocalDateTime startTime){
            this.startTime = startTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
            return this;
        }

        public Snowflake build() {
            if (this.serialBitCount == null) {
                this.serialBitCount = 12;
            }
            if (this.characteristic == null) {
                this.characteristic = getHostCharacteristic();
            }
            return new Snowflake(this.serialBitCount, this.characteristic, this.startTime);
        }
    }

    /**
     * 获取主机标识
     * @return
     */
    private static int getHostCharacteristic() {
        String processorId = SystemUtils.getProcessorId();
        if (processorId == null) {
            return IpUtil.ip4vToInt(IpUtil.getLocalIp());
        }
        byte[] bytes = DataUtil.hexToBytes(processorId);
        long l = DataUtil.byteToLong(bytes);
        return (int)(l & 0x3FFFFF);
    }
}
